Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 405 handler #65

Closed
wants to merge 2 commits into from
Closed

Add 405 handler #65

wants to merge 2 commits into from

Conversation

jchannon
Copy link
Member

@jchannon jchannon commented Nov 16, 2017

Fixes #51

@jchannon jchannon mentioned this pull request Nov 16, 2017
@jchannon
Copy link
Member Author

Pinging @poke to cast his wise eyes over

foreach (var module in modules)
{
foreach (var route in module.Routes.Keys)
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We’re already iterating the module routes in line 48, so we could just combine collecting all “system routes” with setting up the normal route handler here, to avoid iterating everything twice.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially tried to do that but i need to loop to get all routes before applying the 405 handler which is global whereas the route handler is per route

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about something like this:

foreach (var route in …)
{
    routeBuilder.MapVerb(…);
    knownRoutes.Add(route);
}

builder.UseRouter(routeBuilder.Build());
builder.Use(GetMethodNotAllowedHandler(knownRoutes));

}
}

builder.Use(async (context, next) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand it correctly, then this handler is supposed to “pick up” those routes which paths are registered but are using the incorrect verb? In that case, wouldn’t it be better to run this middleware after the normal mapped routes, so it only runs when handling those is over (similarly how in standard ASP.NET, MVC runs after StaticFiles if StaticFiles could not respond to a request).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it runs after, the routing middleware will set 404 as the status code, the issue is, how do you know its a 404 from an unfound route vs a 404 wrongly assigned for invalid verb on path?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I’m completely wrong (routing is one of the topics I didn’t really do anything with yet) but from how it looks, the next middleware is only invoked if there is no matching route. So if the middleware is after the router, it will only run for unrecognized routes. So you would only have to check whether any route with such a path exists in order to determine whether it’s a real not found or just an incorrect verb. So you could do this with a simple hash set on the paths.

? context.Request.Path.Value.Substring(0, context.Request.Path.Value.Length - 1)
: context.Request.Path.Value;

var verbsForPath = systemRoutes.Where(x => x.route == strippedPath).Select(y => y.verb);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you’re looking up routes by paths, then you shouldn’t use a list. Consider an ILookup<string, string> instead that maps from paths to the registered verbs.

: context.Request.Path.Value;

var verbsForPath = systemRoutes.Where(x => x.route == strippedPath).Select(y => y.verb);
if (verbsForPath.All(x => x != context.Request.Method))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per above: If this would run after the normal handlers as a fallback, then at this point you would be already guaranteed that this is running on an unsupported method. So you wouldn’t actually need to lookup (or even store) the registered methods but just the paths.

Copy link
Contributor

@poke poke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One final question: How should your 405 middleware behave when a module’s route handler deliberately returns a 404 or 405 result on its own? E.g. an API module that will return 404 for a missing resource?

{
var moduleType = module.GetType();

foreach (var route in module.Routes.Keys)
{
routeBuilder.MapVerb(route.verb, route.path, CreateRouteHandler(route, moduleType));

var strippedPath = route.path.EndsWith("/") ? route.path.Substring(0, route.path.Length - 1) : route.path;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what David would say about this (allocations!?!?!), but maybe just route.path.TrimEnd('/')?

{
var moduleType = module.GetType();

foreach (var route in module.Routes.Keys)
{
routeBuilder.MapVerb(route.verb, route.path, CreateRouteHandler(route, moduleType));

var strippedPath = route.path.EndsWith("/") ? route.path.Substring(0, route.path.Length - 1) : route.path;
systemRoutes.Add((route.verb, "/" + strippedPath));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be cheaper to leave off the leading slash here, and trim the leading slash from the current request later instead.

/// <param name="next"></param>
/// <param name="systemRoutes"></param>
/// <returns></returns>
private static async Task GetMethodNotAllowedHandler(HttpContext context, Func<Task> next, IEnumerable<(string verb, string route)> systemRoutes)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this does not return a handler function (which is what I was originally suggesting) but is the handler, the Get feels out of place here. HandleMethodNotAllowed? Or maybe you should just move this out to a custom middleware class.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah tried your approach but no joy

private static async Task GetMethodNotAllowedHandler(HttpContext context, Func<Task> next, IEnumerable<(string verb, string route)> systemRoutes)
{
//Call the final pipeline which gets ASP.Net Core status code, usually a 404 in this case
await next();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you continuing the pipeline here at the beginning? Shouldn’t this handler already be kind of the final one in the pipeline? Do you need to know that it produced a 404? Wouldn’t it be safe to assume that if this handler runs after the routing middleware, then a route was not found? (again: I have no actual idea about the routing middleware)

Copy link
Member Author

@jchannon jchannon Nov 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oddly before that is called the response status code is 200, if you call next it's then 404 from the router. Not sure how because as you say I'd expect it to be already at the end of the pipeline

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think you actually need to check the status code here though. This should not run if the router succeeded in the pipeline step before.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly if I remove the next() call it will always be 200 on a route not defined in Botwin. If I take the GetMethodNotAllowedHandler out completely I get a 404 so I assume there must be one last pipeline called somewhere

: context.Request.Path.Value;

//ASP.Net Core will set a 405 response to 404. Let's check if it's a valid 404 first otherwise if we know about the route it's most likely a 405
if (context.Response.StatusCode == 404 && systemRoutes.Any(x => x.route == strippedPath))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use HashSet or Dictionary for constant time lookup. Also, I don’t know if the router middleware is case insensitive itself but if it is, then pure string equality will not work (you will have to use the RouteComparer we came up with earlier).

@jchannon
Copy link
Member Author

Fixed in #66

@jchannon jchannon closed this Nov 19, 2017
@jchannon jchannon deleted the 405 branch November 19, 2017 21:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants